package nancal.mp.config.filter

import nancal.mp.config.base.MyGatewayRouteRepository
import nancal.mp.db.mongo.entity.sys.ApiRouterDefine
import nbcp.base.extend.AsString
import nbcp.base.extend.HasValue
import nbcp.base.extend.Important
import nbcp.base.extend.basicSame
import nbcp.base.model.HttpInvoke
import nbcp.base.model.HttpResponseModel
import nbcp.base.utils.FileUtil
import nbcp.base.utils.JarUtil
import nbcp.base.utils.SpringUtil
import nbcp.base.utils.UrlUtil
import nbcp.flux.ClientIp
import nbcp.flux.fullUrl
import nbcp.flux.getHeader
import nbcp.flux.queryJson
import org.slf4j.LoggerFactory
import org.springframework.cloud.gateway.handler.predicate.ReadBodyRoutePredicateFactory
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.codec.HttpMessageReader
import org.springframework.http.server.reactive.ServerHttpRequest
import org.springframework.http.server.reactive.ServerHttpResponse
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import reactor.core.publisher.Mono
import java.io.File
import java.nio.charset.StandardCharsets
import java.time.LocalDateTime


@Configuration
class ApiRouteWebFilter {
    companion object {
        private val logger = LoggerFactory.getLogger(this::class.java.declaringClass)
        val systemHeaderNames = arrayOf("content-length", "host", "referer", "connection", "origin")
        var messageReaders = listOf<HttpMessageReader<*>>()
    }

    @Bean
    fun routeApiFilter(): WebFilter {
        return WebFilter { exchange, chain ->
            var path = exchange.request.path.value();
            var list = MyGatewayRouteRepository.apiRoutes.sortedBy { it.sourcePath.length };
            var matched = list.firstOrNull { path.startsWith(it.sourcePath) }
            if (matched == null) {
                return@WebFilter chain.filter(exchange);
            }


            var request = exchange.request as ServerHttpRequest;
            var response = exchange.response as ServerHttpResponse;


            if (matched.whenHost.HasValue) {

                //优先使用 gateway-host
                var host = request.getHeader("gateway-host").AsString();
                if (host.HasValue && host != matched.whenHost) {
                    return@WebFilter chain.filter(exchange);
                }

                host = request.getHeader("host");
                if (host.HasValue && host != matched.whenHost) {
                    return@WebFilter chain.filter(exchange);
                }

                host = request.getHeader("x-forwarded-host").AsString();

                if (host.HasValue && host != matched.whenHost) {
                    return@WebFilter chain.filter(exchange);
                }
            }

            if (matched.whenHeaders.isNotEmpty()) {
                for (whenHeader in matched.whenHeaders) {
                    var header = whenHeader.key;
                    var matchValue = whenHeader.value;
                    var headerValue = request.getHeader(header).AsString()

                    if (headerValue == matchValue) {
                        continue
                    }

                    if (Regex(matchValue).matches(headerValue)) {
                        continue
                    }

                    return@WebFilter chain.filter(exchange);
                }
            }


            return@WebFilter getRequestBody(exchange) { requestBody ->
                transformRequest(request, response, requestBody, matched)
            }
        }
    }


    /**
     * 获取请求体。
     */
    private fun getRequestBody(exchange: ServerWebExchange, bodyAction: (String) -> String): Mono<Void> {

        if (messageReaders.isEmpty()) {
            var pf = SpringUtil.getBean<ReadBodyRoutePredicateFactory>();
            var pfType = ReadBodyRoutePredicateFactory::class.java;
            var field = pfType.getDeclaredField("messageReaders");
            field.isAccessible = true;
            messageReaders = field.get(pf) as List<HttpMessageReader<*>>;
        }

        var response = exchange.response as ServerHttpResponse;
        return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange, { serverHttpRequest ->
            ServerRequest
                .create(exchange.mutate().request(serverHttpRequest).build(), messageReaders)
                .bodyToMono<String>(String::class.java)
                .defaultIfEmpty("")
                .doOnNext { }
                .flatMap<Any?> { bodyString: String? ->

                    var resp = "";

                    try {
                        resp = bodyAction.invoke(bodyString.AsString())
                    } catch (e: Exception) {
                        logger.error(e.message, e);

                        response.rawStatusCode = 500;

                        resp = e.javaClass.name + ":" + e.message.AsString()
                    }

                    return@flatMap response.writeWith(
                        Mono.just(
                            response.bufferFactory().wrap(
                                resp.AsString()
                                    .toByteArray(StandardCharsets.UTF_8)
                            )
                        )
                    )
                }
        }) as Mono<Void>
    }


    private fun transformRequest(
        request: ServerHttpRequest,
        response: ServerHttpResponse,
        body: String,
        matched: ApiRouterDefine
    ): String {
        var search = request.fullUrl.split("?")
            .run {
                if (this.size <= 1) {
                    return@run "";
                }

                return@run "?" + this.get(1);
            }

        var http = HttpInvoke()
        http.url = matched.targetPath + search
        for (header in request.headers) {
            var headerName = header.key;
            if (systemHeaderNames.any { it basicSame headerName }) {
                continue
            }
            http.addHeader(headerName, request.getHeader(headerName))
        }


        if (body.HasValue) {
            http.postString = body;
        }

        http.requestMethod = request.method.name

        var ret: HttpResponseModel;
        try {
            ret = http.doInvoke()
        } catch (e: Exception) {
            response.rawStatusCode = 500;
            logger.error("请求失败! " + e.message + "\n${http.url}", e)
            throw e;
        }


        response.rawStatusCode = ret!!.status;

        for (header in ret.responseHeader) {
            var headerName = header.key;
            if (systemHeaderNames.any { it basicSame headerName }) {
                continue
            }
            response.headers.set(headerName, header.value)
        }


        var msg = "自定义转发请求: ${matched.sourcePath} --> ${matched.targetPath}, status: ${ret.status}"
        if (ret.status != 200 && ret.responseText.AsString().HasValue) {
            msg += ", 返回消息: " + ret.responseText.AsString()
        }

        logger.Important(msg)

        return ret.responseText.AsString()
    }


    @Bean
    fun devTraceFilter(): WebFilter {
        return WebFilter { exchange, chain ->
            var path = exchange.request.path.value();
            if (!path.startsWith("/dev-trace")) {
                return@WebFilter chain.filter(exchange);
            }


            return@WebFilter getRequestBody(exchange) { requestBody ->
                showRequestInfo(exchange, requestBody)
            }
        }
    }


    private fun showRequestInfo(
        exchange: ServerWebExchange,
        body: String
    ): String {
        var request = exchange.request as ServerHttpRequest;
        var response = exchange.response as ServerHttpResponse;


        var path = UrlUtil.decodeURIComponent(request.fullUrl);

        var logFile = File(FileUtil.resolvePath(JarUtil.getStartingJarFile().parent, "dev-trace.log"));

        var lines = mutableListOf<String>();

        lines.add("### " + LocalDateTime.now().AsString() + " " + exchange.ClientIp);
        lines.add(request.method.name + " " + path);
        lines.addAll(
            request.headers
                .filterNot { it.key.startsWith("sec-", ignoreCase = true) }
                .map { it.key + ": " + it.value.joinToString(",") }
        );

        if (body.HasValue) {
            lines.add("")
            lines.add(body);
        }

        logFile.appendText(lines.joinToString("\n") + "\n");

        response.rawStatusCode = 200

        var retJson = request.queryJson.get("ret-json").AsString();
        if (retJson.HasValue) {
            response.headers.contentType = org.springframework.http.MediaType.APPLICATION_JSON;
            return retJson;
        }

        response.headers.contentType = org.springframework.http.MediaType.TEXT_PLAIN;
        return lines.joinToString("\n")
    }
}